/*
 *  $Id: TilesImageBlock.java 3 2008-11-18 20:02:30Z Nathaniel.Waisbrot $
    Copyright (C) 2008  Nathaniel Waisbrot

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package termint.gui.graphics;

import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import termint.Main;
import termint.gui.vt.VTElement;
import termint.util.Pair;

/**
 * Holds a VTElement->Image mapping which works with a single large source image containing many little tiles,
 * like the various NetHack tiles distributions.
 * 
 * Note that the data in a VTElement is <i>not</i> sufficient to fully differentiate some tiles. 
 *  
 * @author Nathaniel Waisbrot
 *
 */
public class TilesImageBlock extends Tiles {
	private final Logger log = LoggerFactory.getLogger(getClass());
	
	/** File name of the image block we're using */
	private static final String imageFile = "chozo32b.bmp";
	
	private static final String tileMappingFile = "tile-mapping.json";
	
	/** Width in pixels of each image of {@link TilesImageBlock#imageFile} */
	private static final int imageWidth = 32;
	/** Height in pixels of each image of {@link TilesImageBlock#imageFile} */
	private static final int imageHeight = 32;
	/** Number of columns of sub-images contained in {@link TilesImageBlock#imageFile} */
	private static final int imageBlockColumns = 40;
	
	/** Image shown when the mapping does not contain any data for the given key */
	private final Image badImage;
	
	/** The large image, loaded into memory */
	private Image bigImage;

	/** Mapping between VTElements and coordinates into the {@link TilesImageBlock#bigImage} */
	protected Map<VTElement, Point> elementToImage;

	private final TilesTextColor textTiles;
	
	/** File in which to store the {@link TilesImageBlock#elementToImage} mapping */
	private static final String dataFile = Main.localDir+"tileData";
	
	/**
	 * Create a new mapping
	 * @param display the display on which to draw
	 * @param background the background color to use for tiles
	 */
	public TilesImageBlock(Display display, Color background) {
		super(display, background);
		setTileSize(32, 32);
		
		textTiles = new TilesTextColor(display, 30, new Pair<>(32,32));
				
		badImage = newImage();
		GC gc = getGC(badImage);
		gc.setForeground(display.getSystemColor(SWT.COLOR_RED));
		gc.drawLine(4, 4, 24, 24);
		gc.drawLine(24, 4, 4, 24);

		// set null image to the background color
		nullImage = newImage();
		gc = getGC(nullImage);
		gc.dispose();

		// a little goofyness to make a transparent cursor
		Image cur = newImage();
		gc = new GC(cur);
		gc.setBackground(new Color(display, 0, 0, 0x99));
		gc.fillRectangle(cur.getBounds());
		gc.dispose();
		ImageData d = cur.getImageData();
		d.alpha = 90;
		cur.dispose();
		cur = new Image(display, d);
		setCursor(cur);
		
		// #### NEW STUFF #####
		InputStream is = this.getClass().getResourceAsStream(imageFile);
		bigImage = new Image(display, is);
		elementToImage = loadFromJsonFile();
		Iterator<Entry<VTElement, Point>> iter = elementToImage.entrySet().iterator();
		while (iter.hasNext()) {
			Entry<VTElement, Point> e = iter.next();
			setImage(e.getKey(), e.getValue());
		}
	}	

	/**
	 * Assuming that the ImageData is a grid of images, each one (imageHeight * imageWidth),
	 * extract and return the image at the given grid location.
	 * @param x
	 * @param y
	 * @return
	 */
	protected Image getIconAt(int x, int y) {
		if (x >= imageBlockColumns)
			throw new ArrayIndexOutOfBoundsException("getIconAt("+x+","+y+"): x exceeds max val "+imageBlockColumns);
		Image i = new Image(display, imageWidth, imageHeight);
		GC g = new GC(i);
		try {
			g.drawImage(bigImage, x*imageWidth, y*imageHeight, imageWidth, imageHeight, 0, 0, imageHeight, imageWidth);
		} catch (Exception e) {
			throw new RuntimeException("Failed to draw image from "+x+","+y, e);
		}
		g.dispose();
		return i;
	}
	
	/**
	 * @see TilesImageBlock#getIconAt(int, int)
	 * @param p
	 * @return
	 */
	protected Image getIconAt(Point p) {
		return getIconAt(p.x, p.y);
	}
	
	protected Point indexToXY(int n) {
		int row = 0;
		while (n >= imageBlockColumns) {
			row++;
			n -= imageBlockColumns;
		}
		return new Point(n, row);
	}
	
	protected Image getNthIcon(int n) {
		Point p = indexToXY(n);
		return getIconAt(p.x, p.y);
	}
	
	/**
	 * Set the mapping for the given VTElement to be the <i>n<super>th</super></i> image of the large image 
	 * @param element
	 * @param index
	 */
	public void setImage(VTElement element, int index) {
		if (index < 0) {
			setImage(element, badImage);
			elementToImage.remove(element);
		} else 
			setImage(element, indexToXY(index));
	}
	
	public void setImage(VTElement element, Point xy) {
			setImage(element, getIconAt(xy));
			elementToImage.put(element, xy);
	}

	/**
	 * Set up a GraphicsContext that will draw on the image.  Sets the font, background, etc.
	 * @param img a blank Image to draw on
	 * @return a GC that draws on the Image
	 */
	private GC getGC(Image img) {
		GC g = new GC(img);
		g.setBackground(background);
		g.fillRectangle(img.getBounds());
		return g;
	}
	
	private final Set<VTElement> observedBadElements = new HashSet<>();
	@Override
	public Image getImage(VTElement key) {
		if (key.getChar() == '@')
			log.debug("here");
		Image img = super.getImage(key);
		if ((img != nullImage) || (key.isNull()))
			return img;
		if (!observedBadElements.contains(key)) {
			observedBadElements.add(key);
			log.warn("Missing image for VTElement '{}'", key.toJSON());
		}
		return textTiles.getImage(key);
	}
	
	public void saveData() {
		saveMapping();
	}
	
	@Override
	public void dispose() {
		super.dispose();
		saveData();
		bigImage.dispose();
		badImage.dispose();
	}
		
	
	private static final String mapfile_start_banner = "==START ::$Rev$:: ==";
	private static final String mapfile_end_banner = "==END==";
	
	/**
	 * Save the object -> tile mapping
	 */
	protected void saveMapping() {
		PrintWriter mapOut = null;
		try {
			mapOut = new PrintWriter(new BufferedWriter(new FileWriter(dataFile)));
		} catch (FileNotFoundException e) {
			assert false : "saveMapping: FileNotFound: "+e;
		} catch (IOException e) {
			assert false : "saveMapping: IOException: "+e;
		}
			
		mapOut.println(mapfile_start_banner);
		
		Iterator<Entry<VTElement, Point>> itr = elementToImage.entrySet().iterator();
		while (itr.hasNext()) {
			Entry<VTElement, Point> e = itr.next();
				mapOut.println(e.getKey().toString()+"\t"+e.getValue().x+','+e.getValue().y);
		}
		
		mapOut.println(mapfile_end_banner);
		mapOut.flush();
		mapOut.close();
	}
	
	private Map<VTElement, Point> loadFromJsonFile() {
		try {
			Map<VTElement,Point> map = new HashMap<VTElement,Point>();
			InputStream is = this.getClass().getResourceAsStream(tileMappingFile);
			if (is != null) {
				JSONArray elements = new JSONArray(new JSONTokener(new InputStreamReader(is)));
				for (int i = 0; i < elements.length(); i++) {
					JSONObject jmap = elements.getJSONObject(i);
					VTElement element = VTElement.fromJSON(jmap.getJSONObject("vte"));
					Point p = new Point(jmap.getInt("x"), jmap.getInt("y"));
					map.put(element, p);
				}
			}
			return map;
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
	}
}
